#include "CLogger.h"
#include "CConfigParser.h"
#include "CProcessAttribute.h"
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <time.h>
#include <errno.h>

int CLogger::m_snLogLevel;	// 日志级别
int CLogger::m_snLogFd;		// 文件描述符

char CLogger::m_szErrLevels[][20] = {
	{"stderr"},    //0：控制台错误
	{"emerg"},     //1：紧急
	{"alert"},     //2：警戒
	{"crit"},      //3：严重
	{"error"},     //4：错误
	{"warn"},      //5：警告
	{"notice"},    //6：注意
	{"info"},      //7：信息
	{"debug"}      //8：调试
};

void CLogger::logInit()
{
	char* lpszLogName = NULL;
	size_t nLen;

	// 从配置文件中读取相关配置
	CConfigParser* pConfig = CConfigParser::getInstance();
	lpszLogName = (char*)pConfig->getString("Log");

	if (lpszLogName == NULL)
	{
		lpszLogName = (char*)ERROR_LOG_PATH;
	}
	m_snLogLevel = pConfig->getIntDefault("LogLevel", LOG_NOTICE);	//缺省日志等级为6
	m_snLogFd = open(lpszLogName, O_WRONLY | O_APPEND | O_CREAT, 0644);
	if (m_snLogFd == -1)
	{
        CLogger::logStderr(errno, "[alert] could not open error log file: open() \"%s\" failed", lpszLogName);
		m_snLogFd = STDERR_FILENO;
	}
}

void CLogger::logStderr(int iError, const char* pszfmt, ...)
{
    va_list args;
    u_char szErrStr[MAX_ERROR_STR + 1];

    u_char* p, * last;

    memset(szErrStr, 0, sizeof(szErrStr));

    last = szErrStr + MAX_ERROR_STR;

    p = copymem(szErrStr, LOG_LINE_TITLE, LOG_LINE_TITLE_LEN);
    va_start(args, pszfmt); //使args指向起始的参数
    p = CLogger::vslprintf(p, last, pszfmt, args);
    va_end(args);

    if (iError) {    //如果错误代码不是0，表示有错误发生
        //错误代码和错误信息也要显示出来
        p = logErrno(p, last, iError);
    }

    //若位置不够，那换行也要硬插入到末尾，哪怕覆盖到其他内容
    if (p >= (last - 1)) {
        p = (last - 1) - 1;     //last-1，才是最后 一个而有效的内存，而这个位置要保存\0，所以我认为再减1，这个位置，才适合保存\n
    }
    *p++ = '\n';    //增加个换行符

    //往标准错误【一般是屏幕】输出信息
    write(STDERR_FILENO, szErrStr, p - szErrStr);
}

//往日志文件中写日志，代码中有自动加换行符，所以调用时字符串不用刻意加\n
//日志定向为标准错误，则直接往屏幕上写日志【比如日志文件打不开，则会直接定位到标准错误，此时日志就打印到屏幕上，参考logInit()】
//iLevel:一个等级数字，我们把日志分成一些等级，以方便管理、显示、过滤等等，如果这个等级数字比配置文件中的等级数字"LogLevel"大，那么该条信息不被写到日志文件中
//iError：是个错误代码，如果不是0，就应该转换成显示对应的错误信息,一起写到日志文件中
//logErrorCore(5,8,"这个XXX工作的有问题,显示的结果是=%s", "YYYY")
void CLogger::logErrorCore(int iLevel, int iError, const char* pszFmt, ...)
{
    u_char* pLast;
    u_char szError[MAX_ERROR_STR + 1];
    memset(szError, 0, sizeof(szError));
    pLast = szError + MAX_ERROR_STR;

    struct timeval tv;
    struct tm   tm;
    time_t      sec;    //秒
    u_char* p;     //指向当前要拷贝数据到其中的内存位置
    va_list     args;

    memset(&tv, 0, sizeof(struct timeval));
    memset(&tm, 0, sizeof(struct tm));

    gettimeofday(&tv, NULL);    //获取当前时间，返回自1970-01-01 00:00:00到现在经历的秒数【第二个参数是时区，一般不关心】

    sec = tv.tv_sec;
    localtime_r(&sec, &tm);     //把参数1的time_t转换为本地时间，保存到参数2中去，带_r的是线程安全的版本，尽量使用
    tm.tm_mon++;                 //月份要调整下正常
    tm.tm_year += 1900;          //年份要调整下才正常

    u_char szCurrTime[40] = { 0 };    //先组合出一个当前时间字符串，格式形如：2019/01/08 19:57:11
    CLogger::slprintf(szCurrTime,
        (u_char*)-1, //若用一个u_char *接一个 (u_char *)-1,则 得到的结果是 0xffffffff....，这个值表示的地址足够大
        "%4d/%02d/%02d %02d:%02d:%02d",     //格式是 年/月/日 时:分:秒
        tm.tm_year, tm.tm_mon,
        tm.tm_mday, tm.tm_hour,
        tm.tm_min, tm.tm_sec);
    p = copymem(szError, szCurrTime, strlen((const char*)szCurrTime));  //日期增加进来，得到形如：2019/01/08 20:26:07
    p = CLogger::slprintf(p, pLast, " [%s] ", m_szErrLevels[iLevel]);        //日志级别增加进来，得到形如：2019/01/08 20:26:07 [crit]
    p = CLogger::slprintf(p, pLast, "%P: ", CProcessAttribute::m_siPid);                       //支持%P格式，进程id增加进来，得到形如：2019/01/08 20:50:15 [crit] 2037:

    va_start(args, pszFmt);                         //使args指向起始的参数
    p = CLogger::vslprintf(p, pLast, pszFmt, args);    //把fmt和args参数弄进去，组合出来这个字符串
    va_end(args);                                   //释放args

    if (iError) {                                    //如果错误代码不是0，表示有错误发生
        p = CLogger::logErrno(p, pLast, iError);             //错误代码和错误信息也要显示出来
    }

    //若位置不够，那换行也要硬插入到末尾，哪怕覆盖到其他内容
    if (p >= (pLast - 1)) {
        p = (pLast - 1) - 1;                        //把尾部空格留出来，这里感觉nginx处理的似乎就不对
                                                    //我觉得，last-1，才是最后 一个而有效的内存，而这个位置要保存\0，所以我认为再减1，这个位置，才适合保存\n
    }
    *p++ = '\n';                                    //增加个换行符

    //这么写代码是图方便：随时可以把流程弄到while后边去，可以借鉴一下这种写法
    ssize_t n;
    while (1) {
        if (iLevel > m_snLogLevel) {
            //要打印的这个日志的等级太落后（等级数字太大，比配置文件中的数字大)
            //这种日志就不打印了
            break;
        }
        //磁盘是否满了的判断，先算了吧，还是由管理员保证这个事情吧
        //写日志文件
        n = write(m_snLogFd, szError, p - szError);
        if (n == -1) {
            //写失败有问题
            if (errno == ENOSPC) //写失败，且原因是磁盘没空间了
            {
                //磁盘没空间了
                //没空间还写个毛线啊
                //先do nothing吧；
            }
            else {
                if (m_snLogFd != STDERR_FILENO) {    //当前是定位到文件的，则条件成立
                    n = write(STDERR_FILENO, szError, p - szError);
                }
            }
        }
        break;
    }
}

//描述：给一段内存，一个错误编号，我要组合出一个字符串，形如：(错误编号: 错误原因)，放到给的这段内存中去
//pszBuf：是个内存，要往这里保存数据
//pszLast：放的数据不要超过这里
//iError：错误编号，我们是要取得这个错误编号对应的错误字符串，保存到buffer中
u_char* CLogger::logErrno(u_char* pszBuf, u_char* pszLast, int iError)
{
    char* pszErrorInfo = strerror(iError);
    size_t iLen = strlen(pszErrorInfo);

    //插入一些字符串
    char szLeft[10] = { 0 };
    sprintf(szLeft, "(%d: ", iError);
    size_t iLeftLen = strlen(szLeft);

    char szRight[] = ") ";
    size_t iRightLen = strlen(szRight);

    size_t iExtraLen = iLeftLen + iRightLen;
    if (pszBuf + iLeftLen + iExtraLen < pszLast) {
        //保证整个我装得下，我就装，否则我全部抛弃 ,nginx的做法是 如果位置不够，就硬留出50个位置【哪怕覆盖掉以往的有效内容】，也要硬往后边塞，这样当然也可以；
        pszBuf = copymem(pszBuf, szLeft, iLeftLen);
        pszBuf = copymem(pszBuf, pszErrorInfo, iLen);
        pszBuf = copymem(pszBuf, szRight, iRightLen);
    }
    return pszBuf;
}

u_char* CLogger::slprintf(u_char* pszBuf, u_char* pszLast, const char* pszFmt, ...)
{
    va_list args;
    u_char* p;

    va_start(args, pszFmt);     //使args指向起始的参数
    p = CLogger::vslprintf(pszBuf, pszLast, pszFmt, args);
    va_end(args);               //释放args
    return p;
}

u_char* CLogger::snprintf(u_char* pszBuf, size_t uMax, const char* pszFmt, ...)
{
    u_char* p;
    va_list args;
    va_start(args, pszFmt);
    p = CLogger::vslprintf(pszBuf, pszBuf + uMax, pszFmt, args);
    va_end(args);
    return p;
}

//例如，给进来一个 "abc = %d",13   ,最终buf里得到的应该是   abc=13 这种结果
//pszBuf：往这里放数据
//pszLast：放的数据不要超过这里
//pszFmt：以这个为首的一系列可变参数
//支持的格式： %d【%Xd/%xd】:数字,    %s:字符串      %f：浮点,  %P：pid_t
//对于：log_stderr(0, "invalid option: \"%s\",%d", "testinfo",123);
//pszFmt = "invalid option: \"%s\",%d"
//args = "testinfo",123
u_char* CLogger::vslprintf(u_char* pszBuf, u_char* pszLast, const char* pszFmt, va_list args)
{
    //比如说你要调用ngx_log_stderr(0, "invalid option: \"%s\"", argv[i]);，那么这里的fmt就应该是:   invalid option: "%s"
    //printf("fmt = %s\n",fmt);
    u_char zero;
    uintptr_t width, sign, hex, fracWidth, scale, n;        //临时用到的一些变量

    int64_t    i64;   //保存%d对应的可变参
    uint64_t   ui64;  //保存%ud对应的可变参，临时作为%f可变参的整数部分也是可以的
    u_char* p;    //保存%s对应的可变参
    double     f;     //保存%f对应的可变参
    uint64_t   frac;  //%f可变参数,根据%.2f等，取得小数部分的2位后的内容；

    while (*pszFmt && pszBuf < pszLast) {
        if (*pszFmt == '%') {
            //-----------------变量初始化工作开始-----------------
            //++pszFmt是先加后用，也就是fmt先往后走一个字节位置，然后再判断该位置的内容
            //判断%后边接的是否是个'0',如果是zero = '0'，否则zero = ' '，一般比如你想显示10位，而实际数字7位，前头填充三个字符，就是这里的zero用于填充
            zero = (u_char)((*++pszFmt == '0') ? '0' : ' ');

            width = 0;              //格式字符% 后边如果是个数字，这个数字最终会弄到width里边来 ,这东西目前只对数字格式有效，比如%d,%f这种
            sign = 1;               //显示的是否是有符号数，这里给1，表示是有符号数，除非你 用%u，这个u表示无符号数
            hex = 0;                //是否以16进制形式显示(比如显示一些地址)，0：不是，1：是，并以小写字母显示a-f，2：是，并以大写字母显示A-F
            fracWidth = 0;          //小数点后位数字，一般需要和%.10f配合使用，这里10就是frac_width；
            i64 = 0;                //一般用%d对应的可变参中的实际数字，会保存在这里
            ui64 = 0;               //一般用%ud对应的可变参中的实际数字，会保存在这里

            //-----------------变量初始化工作结束-----------------

            //这个while就是判断%后边是否是个数字，如果是个数字，就把这个数字取出来，比如%16，最终这个循环就能够把16取出来弄到width里边去
            //%16d 这里最终width = 16;
            while (*pszFmt >= '0' && *pszFmt <= '9') {
                //第一次 ：width = 1;  第二次 width = 16，所以整个width = 16；
                width = width * 10 + (*pszFmt++ - '0');
            }

            for (;;) {   //一些特殊的格式，我们做一些特殊的标记【给一些变量特殊值等等】
                switch (*pszFmt) {  //处理一些%之后的特殊字符
                case 'u':           //%u，这个u表示无符号
                    sign = 0;       //标记这是个无符号数
                    pszFmt++;       //往后走一个字符
                    continue;       //回到for继续判断

                case 'X':           //%X，X表示十六进制，并且十六进制中的A-F以大写字母显示，不要单独使用，一般是%Xd
                    hex = 2;        //标记以大写字母显示十六进制中的A-F
                    sign = 0;
                    pszFmt++;
                    continue;

                case 'x':           //%x，x表示十六进制，并且十六进制中的a-f以小写字母显示，不要单独使用，一般是%xd
                    hex = 1;        //标记以小写字母显示十六进制中的a-f
                    sign = 0;
                    pszFmt++;
                    continue;

                case '.':           //其后边必须跟个数字，必须与%f配合使用，形如 %.10f：表示转换浮点数时小数部分的位数，比如%.10f表示转换浮点数时，小数点后必须保证10位数字，不足10位则用0来填补；
                    pszFmt++;
                    while (*pszFmt >= '0' && *pszFmt <= '9') {
                        fracWidth = fracWidth * 10 + (*pszFmt++ - '0');
                    }
                    break;

                default:
                    break;
                }
                break;
            }

            switch (*pszFmt) {
            case '%':       //只有%%时才会遇到这个情形，本意是打印一个%，所以
                *pszBuf++ = '%';
                pszFmt++;
                continue;

            case 'd':       //显示整型数据，如果和u配合使用，也就是%ud,则是显示无符号整型数据
                if (sign) {   //如果是有符号数
                    i64 = (int64_t)va_arg(args, int);  //va_arg():遍历可变参数，var_arg的第二个参数表示遍历的这个可变的参数的类型
                }
                else {    //如果是和 %ud配合使用，则本条件就成立
                    ui64 = (uint64_t)va_arg(args, u_int);
                }
                break;

            case 's':       //一般用于显示字符串
                p = va_arg(args, u_char*); //va_arg():遍历可变参数，var_arg的第二个参数表示遍历的这个可变的参数的类型
                while (*p && pszBuf < pszLast) {    //没遇到字符串结束标记，并且buf值够装得下这个参数
                    *pszBuf++ = *p++;   //那就装，比如  "%s"    ，   "abcdefg"，那abcdefg都被装进来
                }
                pszFmt++;
                continue;   //重新从while开始执行

            case 'P':       //转换一个pid_t类型
                i64 = (int64_t)va_arg(args, pid_t);
                sign = 1;
                break;

            case 'f':       //一般 用于显示double类型数据，如果要显示小数部分，则要形如 %.5f
                f = va_arg(args, double);   //va_arg():遍历可变参数，var_arg的第二个参数表示遍历的这个可变的参数的类型
                if (f < 0) { //负数的处理
                    *pszBuf++ = '-';    //单独搞个负号出来
                    f = -f;             //那这里f应该是正数了!
                }
                //走到这里保证f肯定 >= 0【不为负数】
                ui64 = (int64_t)f;      //正整数部分给到ui64里
                frac = 0;

                //如果要求小数点后显示多少位小数
                if (fracWidth) { //如果是%d.2f，那么frac_width就会是这里的2
                    scale = 1;
                    for (n = fracWidth; n; n--) {
                        scale *= 10;
                    }
                    //把小数部分取出来 ，比如如果是格式    %.2f   ，对应的参数是12.537
                    // (uint64_t) ((12.537 - (double) 12) * 100 + 0.5)
                    // = (uint64_t) (0.537 * 100 + 0.5)
                    // = (uint64_t) (53.7 + 0.5)
                    // = (uint64_t) (54.2)
                    // = 54
                    frac = (uint64_t)((f - (double)ui64) * scale + 0.5);    //取得保留的那些小数位数，【比如%.2f，对应的参数是12.537，取得的就是小数点后的2位四舍五入，也就是54】
                                                                            //如果是"%.6f", 21.378，那么这里frac = 378000

                    //进位，比如%.2f，对应的参数是12.999，那么 = (uint64_t) (0.999 * 100 + 0.5) = (uint64_t) (99.9 + 0.5) = (uint64_t) (100.4) = 100
                    if (frac == scale) {
                        ui64++;
                        frac = 0;
                    }
                }
                //正整数部分，先显示出来
                pszBuf = CLogger::sprintfNum(pszBuf, pszLast, ui64, zero, 0, width);   //把一个数字 比如“1234567”弄到buffer中显示
                if (fracWidth) { //指定了显示多少位小数
                    if (pszBuf < pszLast) {
                        *pszBuf++ = '.';    //因为指定显示多少位小数，先把小数点增加进来
                    }
                    pszBuf = CLogger::sprintfNum(pszBuf, pszLast, frac, '0', 0, fracWidth);
                }
                pszFmt++;
                continue;   //重新从while开始执行

            default:
                *pszBuf++ = *pszFmt++;  //往下移动一个字符
                continue;   //注意这里不break，而是continue;而这个continue其实是continue到外层的while去了，也就是流程重新从while开头开始执行;
            }

            //显示%d的，会走下来，统一把显示的数字都保存到 ui64 里去；
            if (sign) //显示的是有符号数
            {
                if (i64 < 0) {
                    *pszBuf++ = '-';    //小于0，自然要把负号先显示出来
                    ui64 = (uint64_t)-i64; //变成无符号数（正数）
                }
                else {    //显示正数
                    ui64 = (uint64_t)i64;
                }
            }

            //把一个数字 比如“1234567”弄到buffer中显示，如果是要求10位，则前边会填充3个空格比如“   1234567”
            //注意第5个参数hex，是否以16进制显示，比如如果你是想以16进制显示一个数字则可以%Xd或者%xd，此时hex = 2或者1
            pszBuf = CLogger::sprintfNum(pszBuf, pszLast, ui64, zero, hex, width);
            pszFmt++;
        }
        else {    //当成正常字符，源【fmt】拷贝到目标【buf】里
         //用pszFmt当前指向的字符赋给pszBuf当前指向的位置，然后pszBuf往前走一个字符位置，pszFmt往前走一个字符位置
            *pszBuf++ = *pszFmt++;  //*和++优先级相同，结合性从右到左，所以先求的是buf++以及fmt++，但++是先用后加；
        }
    }
    return pszBuf;
}

//以一个指定的宽度把一个数字显示在buf对应的内存中, 如果实际显示的数字位数 比指定的宽度要小 ,比如指定显示10位，而你实际要显示的只有“1234567”，那结果可能是会显示“   1234567”
//当然如果你不指定宽度【参数width=0】，则按实际宽度显示
//你给进来一个%Xd之类的，还能以十六进制数字格式显示出来
//buf：往这里放数据
//last：放的数据不要超过这里
//ui64：显示的数字
//zero:显示内容时，格式字符%后边接的是否是个'0',如果是zero = '0'，否则zero = ' ' 【一般显示的数字位数不足要求的，则用这个字符填充】，比如要显示10位，而实际只有7位，则后边填充3个这个字符；
//hexadecimal：是否显示成十六进制数字 0：不
//width:显示内容时，格式化字符%后接的如果是个数字比如%16，那么width=16，所以这个是希望显示的宽度值【如果实际显示的内容不够，则后头用0填充】
u_char* CLogger::sprintfNum(u_char* pBuf, u_char* pLast, uint64_t ui64, u_char zero, uintptr_t hexDecimal, uintptr_t width)
{
    u_char* p, pszTemp[INT64_LEN + 1];
    size_t len;
    uint32_t ui32;

    static u_char hex[] = "0123456789abcdef";   //跟把一个10进制数显示成16进制有关，换句话说和  %xd格式符有关，显示的16进制数中a-f小写
    static u_char HEX[] = "0123456789ABCDEF";   //跟把一个10进制数显示成16进制有关，换句话说和  %Xd格式符有关，显示的16进制数中A-F大写

    p = pszTemp + INT64_LEN;    //INT64_LEN = 20,所以p指向的是temp[20]那个位置，也就是数组最后一个元素位置

    if (hexDecimal == 0) {
        if (ui64 <= (uint64_t)MAX_UINT32_VALUE) {    //MAX_UINT32_VALUE :最大的32位无符号数：十进制是‭4294967295‬ ui32能存下
            ui32 = (uint32_t)ui64;

            //这个循环能够把诸如 7654321这个数字保存成：temp[13]=7,temp[14]=6,temp[15]=5,temp[16]=4,temp[17]=3,temp[18]=2,temp[19]=1
            //而且的包括temp[0..12]以及temp[20]都是不确定的值
            do {
                *--p = (u_char)(ui32 % 10 + '0');   //把屁股后边这个数字拿出来往数组里装，并且是倒着装：屁股后的也往数组下标大的位置装；
            } while (ui32 /= 10);   //每次缩小10倍等于去掉屁股后边这个数字
        }
        else {    //如果ui32存不下
            do {
                *--p = (u_char)(ui32 % 10 + '0');
            } while (ui64 /= 10);
        }
    }
    else if (hexDecimal == 1) { //如果显示一个十六进制数字，格式符为：%xd，则这个条件成立，要以16进制数字形式显示出来这个十进制数,a-f小写

        //比如我显示一个1,234,567【十进制数】，他对应的二进制数实际是 12 D687 ，那怎么显示出这个12D687来呢？
        //ui64 & 0xf  其实就能分别得到 这个16进制数也就是 7,8,6,D,2,1这个数字，转成 (uint32_t) ，然后以这个为hex的下标，找到这几个数字的对应的能够显示的字符；
        do {
            *--p = hex[(uint32_t)(ui64 & 0xf)];
        } while (ui64 >>= 4);
        //ui64 >>= 4     --->   ui64 = ui64 >> 4 ,而ui64 >> 4是啥，实际上就是右移4位，就是除以16,因为右移4位就等于移动了1111；
        //相当于把该16进制数的最末尾一位干掉，原来是 12 D687, >> 4后是 12 D68，如此反复，最终肯定有=0时导致while不成立退出循环
        //比如 1234567 / 16 = 77160(0x12D68) 77160 / 16 = 4822(0x12D6)

    }
    else {  // hexadecimal == 2 如果显示一个十六进制数字，格式符为：%Xd，则这个条件成立，要以16进制数字形式显示出来这个十进制数,A-F大写
        //参考else if (hexadecimal == 1)，非常类似
        do {
            *--p = HEX[(uint32_t)(ui64 & 0xf)];
        } while (ui64 >>= 4);
    }

    len = (pszTemp + INT64_LEN) - p;

    while (len++ < width && pBuf < pLast) {     //如果你希望显示的宽度是12个宽度【%12f】，而实际想显示的是7654321，只有7个宽度，那么这里要填充5个0进去到末尾，凑够要求的宽度
        //填充0进去到buffer中（往末尾增加），比如你用格式log_stderr(0, "invalid option: %10d\n", 21);
        //显示的结果是：nginx: invalid option:         21  ---21前面有8个空格，这8个弄个，就是在这里添加进去的；
        *pBuf++ = zero;
    }

    len = (pszTemp + INT64_LEN) - p;    //还原这个len，也就是要显示的数字的实际宽度【因为上边这个while循环改变了len的值】
                                        //现在还没把实际的数字比如“7654321”往buf里拷贝呢，要准备拷贝
    if ((pBuf + len) >= pLast) {
        len = pLast - pBuf; //剩余的buf有多少我就拷贝多少
    }

    return copymem(pBuf, p, len);
}

void CLogger::deInit()
{
    if (m_snLogFd != STDERR_FILENO && m_snLogFd != -1)
    {
        close(m_snLogFd);
        m_snLogFd = -1;
    }
}
